﻿using System;
using System.Diagnostics;
using System.IO;

namespace SfsTools.Core
{
    /// <summary>
    /// Represents a file on a <see cref="FileSystem"/>.
    /// </summary>
    internal class File : FileSystemObject, IFile
    {
        #region Properties

        /// <summary>
        /// Contains the count of open streams to the file.
        /// </summary>
        private int FOpenCount;

        #endregion

        #region Methods

        #region Misc

        /// <summary>
        /// Notifies the <see cref="File"/> that a <see cref="FileStream"/> has been closed.
        /// </summary>
        [DebuggerStepThrough]
        public void Close()
        {
            FOpenCount--;
            Debug.Assert(FOpenCount >= 0);
        }

        /// <summary>
        /// Notifies the <see cref="File"/> that a <see cref="FileStream"/> has been opened.
        /// </summary>
        [DebuggerStepThrough]
        public void Open()
        {
            Debug.Assert(FOpenCount >= 0);
            FOpenCount++;
        }

        /// <summary>
        /// Updates <see cref="Length"/>.
        /// </summary>
        [DebuggerStepThrough]
        private void UpdateLength()
        {
            if (FIsDeleted)
            {
                FLength = -1;
            }
            else
            {
                BaseFileEntry entry = (BaseFileEntry)GetEntry();
                FLength = entry.FileSize;
            }
        }

        /// <summary>
        /// Returns a <see cref="String"/> that represents the current <see cref="File"/>.
        /// </summary>
        /// <returns>A <see cref="String"/> that represents the current <see cref="File"/>.</returns>
        [DebuggerStepThrough]
        public override string ToString()
        {
            return String.Concat('{',
                String.Format(Translation.Resources.GetString(typeof(File).Name),
                GetFullPath(), FLength), '}');
        }

        /// <summary>
        /// Accepts a visitor.
        /// </summary>
        /// <param name="visitor">The visitor.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="visitor"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public override void AcceptVisitor(IFileSystemObjectVisitor visitor)
        {
            if (visitor == null)
                throw new ArgumentNullException("visitor");
            visitor.VisitFile(this);
        }
        /// <summary>
        /// Accepts a visitor.
        /// </summary>
        /// <param name="visitor">The visitor.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="visitor"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public override void AcceptVisitor(IFileSystemObjectInternalVisitor visitor)
        {
            if (visitor == null)
                throw new ArgumentNullException("visitor");
            visitor.VisitFile(this);
        }

        #endregion

        #region Delete

        /// <summary>
        /// Deletes this <see cref="IFile"/>.
        /// </summary>
        /// <exception cref="InvalidOperationException">The file is already deleted.</exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        [DebuggerStepThrough]
        public override void Delete()
        {
            CheckAlive();
            // TODO: InvOpExc thrown for two different reasons.
            if (FIsDeleted)
                throw new FileAlreadyDeletedException(GetFullPath());
            if (FOpenCount > 0)
                throw new CannotDeleteOpenFileException(GetFullPath());
            FIsDeleted = true;
            Debug.Assert(FParent != null);
            FParent.DeleteFile(this, FIndexAreaEntryIndex);
            // V1: resurrection is not supported.
            Kill();
        }

        #endregion

        #region Resurrect

        /// <summary>
        /// Restores this <see cref="IFile"/> that was previously deleted.
        /// </summary>
        /// <exception cref="InvalidOperationException">The file is not deleted.</exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        /// <exception cref="IndexAreaFullException">Cannot create directory because the index area is full.</exception>
        [DebuggerStepThrough]
        public override void Resurrect()
        {
            // TODO: check for the case when the data area of a deleted file overlaps
            // with an existing file (ie. the file has been overwritten).
            throw new NotImplementedException();
        }

        #endregion

        #region Rename

        /// <summary>
        /// Renames this file to <paramref name="name"/>.
        /// </summary>
        /// <param name="name">The new name of this <see cref="IFile"/>.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="name"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException">The argument <paramref name="name"/> contains invalid characters.</exception>
        /// <exception cref="InvalidOperationException">The parent directory already contains a file with <paramref name="name"/>.</exception>
        /// <exception cref="IndexAreaFullException">Cannot allocate entries because the index area is full.</exception>
        /// <exception cref="System.IO.PathTooLongException">The full path is too long.</exception>
        [DebuggerStepThrough]
        public override void Rename(String name)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            // TODO: InvOpExc raised for two separate reasons.
            if (FIsDeleted)
                throw new FileAlreadyDeletedException(GetFullPath());
            PathValidator.ValidateDirectoryName(name);
            if (FName == name)
                return;
            if (FParent.Contains(name))
                throw new CannotRenameFileException(GetFullPath());
            // Must have an index.
            Debug.Assert(FIndexAreaEntryIndex > 0);
            Debug.Assert(FParent != null);
            //FIndexAreaEntryIndex = FFileSystem.IndexArea.RenameFile(FIndexAreaEntryIndex, String.Concat(FParent.GetFullPath(), name));
            //FParent.Remove(this);
            //FName = name;
            //FParent.Add(this);
            throw new NotImplementedException();
        }

        #endregion

        #region Read

        /// <summary>
        /// Reads a sequence of bytes from the file.
        /// </summary>
        /// <param name="position">The position to start reading at.</param>
        /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="index"/> and (<paramref name="index"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
        /// <param name="index">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
        /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
        /// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
        /// <exception cref="ArgumentException">The sum of <paramref name="index"/> and <paramref name="count"/> is larger than the buffer length.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="index"/> or <paramref name="count"/> is negative.</exception>
        /// <exception cref="IOException">An I/O error occured.</exception>
        //[DebuggerStepThrough]
        public int Read(long position, byte[] buffer, int index, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if ((index < 0) || (index > buffer.Length))
                throw new ArgumentOutOfRangeException("index");
            if ((count < 0) || (index + count > buffer.Length))
                throw new ArgumentOutOfRangeException("count");
            Debug.Assert(!FIsDeleted);
            Debug.Assert(position >= 0);
            // when to throw EndOfStreamException?
            if (position == FLength)
                return 0;
            if (position > FLength)
                throw new EndOfStreamException();
            // Limit length if we are trying to read past the end of the file.
            if (position + count > FLength)
                count = (int)(FLength - position);
            int blockSize = FFileSystem.BootBlock.BlockSize;
            BaseFileEntry entry = (BaseFileEntry)GetEntry();
            // Read first partial block.
            int read = count;
            if ((position % blockSize) > 0)
            {
                int delta = (int)(position % blockSize);
                int length = blockSize - delta;
                if (length > count)
                    length = count;
                long blockIndex = entry.FirstBlock + position / blockSize;
                byte[] block = FFileSystem.DataArea.Read(blockIndex, 1);
                Array.Copy(block, delta, buffer, index, length);
                index += length;
                count -= length;
                position += length;
            }
            // Read full blocks.
            for (; count >= blockSize; )
            {
                long blockIndex = entry.FirstBlock + position / blockSize;
                byte[] block = FFileSystem.DataArea.Read(blockIndex, 1);
                Array.Copy(block, 0, buffer, index, blockSize);
                index += blockSize;
                count -= blockSize;
                position += blockSize;
            }
            // Read last partial block.
            if (count > 0)
            {
                Debug.Assert(count < blockSize);
                long blockIndex = entry.FirstBlock + position / blockSize;
                byte[] block = FFileSystem.DataArea.Read(blockIndex, 1);
                Array.Copy(block, 0, buffer, index, count);
            }
            return read;
        }

        #endregion

        #region Write

        /// <summary>
        /// Writes a sequence of bytes to the file.
        /// </summary>
        /// <param name="position">The position to start writing at.</param>
        /// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
        /// <param name="index">The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.</param>
        /// <param name="count">The number of bytes to be written to the current stream.</param>
        /// <exception cref="ArgumentException">The sum of <paramref name="index"/> and <paramref name="count"/> is larger than the buffer length.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="index"/> or <paramref name="count"/> is negative.</exception>
        /// <exception cref="IOException">An I/O error occured.</exception>
        public void Write(long position, byte[] buffer, int index, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if ((index < 0) || (index > buffer.Length))
                throw new ArgumentOutOfRangeException("index");
            if ((count < 0) || (index + count > buffer.Length))
                throw new ArgumentOutOfRangeException("count");
            Debug.Assert(!FIsDeleted);
            // Check if we need to grow the file.
            if (position + count > FLength)
                SetLength(position + count);
            int blockSize = FFileSystem.BootBlock.BlockSize;
            BaseFileEntry entry = (BaseFileEntry)GetEntry();
            // Write first partial block.
            if ((position % blockSize) > 0)
            {
                int delta = (int)(position % blockSize);
                int length = blockSize - delta;
                if (length > count)
                    length = count;
                long blockIndex = entry.FirstBlock + position / blockSize;
                byte[] block = FFileSystem.DataArea.Read(blockIndex, 1);
                Array.Copy(buffer, index, block, delta, length);
                FFileSystem.DataArea.Write(blockIndex, block);
                index += length;
                count -= length;
                position += length;
            }
            // Write full blocks.
            for (; count >= blockSize;)
            {
                long blockIndex = entry.FirstBlock + position / blockSize;
                FFileSystem.DataArea.Write(blockIndex, buffer, index, 1);
                index += blockSize;
                count -= blockSize;
                position += blockSize;
            }
            // Write last partial block.
            if (count > 0)
            {
                Debug.Assert(count < blockSize);
                long blockIndex = entry.FirstBlock + position / blockSize;
                byte[] block = FFileSystem.DataArea.Read(blockIndex, 1);
                Array.Copy(buffer, index, block, 0, count);
                FFileSystem.DataArea.Write(blockIndex, block);
            }
        }

        #endregion

        #region Set length

        /// <summary>
        /// Sets the length of the file.
        /// </summary>
        /// <param name="value">The desired length of the file in bytes.</param>
        /// <exception cref="IOException">An I/O error occured.</exception>
        [DebuggerStepThrough]
        public void SetLength(long value)
        {
            // TODO: we are not supposed to throw this exception, so what?
            if (value < 0)
                throw new CannotSetNegativeFileLengthException("value");
            Debug.Assert(!FIsDeleted);
            // Set the file length.
            FFileSystem.IndexArea.SetFileLength(FIndexAreaEntryIndex, value);
            UpdateLength();
        }

        #endregion

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="File"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="File"/>.
        /// </summary>
        /// <param name="fs">The file system.</param>
        /// <param name="parent">The <see cref="Directory"/> this file resides in.</param>
        /// <param name="name">The name of this directory.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="parent"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public File(FileSystem fs, Directory parent, String name)
            : base(fs, parent, name)
        {
            UpdateLength();
        }
        /// <summary>
        /// Initializes a new instance of <see cref="File"/>.
        /// </summary>
        /// <param name="fs">The file system.</param>
        /// <param name="parent">The <see cref="Directory"/> this file resides in.</param>
        /// <param name="name">The name of this directory.</param>
        /// <param name="index">The zero-based index of the index area entry.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="parent"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="index"/> is out of valid range.</exception>
        [DebuggerStepThrough]
        public File(FileSystem fs, Directory parent, String name, int index)
            : base(fs, parent, name, index)
        {
            UpdateLength();
        }
        /// <summary>
        /// Initializes a new instance of <see cref="Directory"/>.
        /// </summary>
        /// <param name="fs">The file system.</param>
        /// <param name="parent">The <see cref="Directory"/> this file resides in.</param>
        /// <param name="name">The name of this directory.</param>
        /// <param name="deleted">Specifies whether this <see cref="File"/> is deleted.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="parent"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public File(FileSystem fs, Directory parent, String name, bool deleted)
            : base(fs, parent, name, deleted)
        {
            UpdateLength();
        }
        /// <summary>
        /// Initializes a new instance of <see cref="Directory"/>.
        /// </summary>
        /// <param name="fs">The file system.</param>
        /// <param name="parent">The <see cref="Directory"/> this file resides in.</param>
        /// <param name="name">The name of this directory.</param>
        /// <param name="index">The zero-based index of the index area entry.</param>
        /// <param name="deleted">Specifies whether this <see cref="File"/> is deleted.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="parent"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="index"/> is out of valid range.</exception>
        [DebuggerStepThrough]
        public File(FileSystem fs, Directory parent, String name, int index, bool deleted)
            : base(fs, parent, name, index, deleted)
        {
            UpdateLength();
        }
        #endregion

        #region IFile Members

        /// <summary>
        /// Contains the current size of the file in bytes.
        /// </summary>
        private long FLength;
        /// <summary>
        /// Gets the current size of the file in bytes.
        /// </summary>
        /// <value>The current size of the file in bytes, or <em>-1</em> if the file is deleted.</value>
        public Int64 Length
        {
            [DebuggerStepThrough]
            get
            {
                return FLength;
            }
        }
        /// <summary>
        /// Gets the current size of the file in bytes.
        /// </summary>
        /// <value>The current size of the file in bytes, or <em>-1</em> if the file is deleted.</value>
        Int64 IFile.Length
        {
            [DebuggerStepThrough]
            get
            {
                CheckAlive();
                return FLength;
            }
        }

        /// <summary>
        /// Opens the file in binary mode for reading.
        /// </summary>
        /// <returns>A <see cref="System.IO.Stream"/> that can be used to read from the file.</returns>
        /// <exception cref="InvalidOperationException">The file is deleted.</exception>
        [DebuggerStepThrough]
        Stream IFile.OpenRead()
        {
            CheckAlive();
            if (FIsDeleted)
                throw new CannotOpenDeletedFileException();
            return new FileStream(this, FileAccess.Read);
        }

        /// <summary>
        /// Opens the file in binary mode for writing.
        /// </summary>
        /// <returns>A <see cref="System.IO.Stream"/> that can be used to write to the file.</returns>
        /// <exception cref="InvalidOperationException">The file is deleted.</exception>
        [DebuggerStepThrough]
        Stream IFile.OpenWrite()
        {
            CheckAlive();
            if (FIsDeleted)
                throw new CannotOpenDeletedFileException();
            return new FileStream(this, FileAccess.Write);
        }

        /// <summary>
        /// Opens the file in binary mode for appending.
        /// </summary>
        /// <returns>A <see cref="System.IO.Stream"/> that can be used to write to the file.</returns>
        /// <exception cref="InvalidOperationException">The file is deleted.</exception>
        [DebuggerStepThrough]
        Stream IFile.OpenAppend()
        {
            CheckAlive();
            if (FIsDeleted)
                throw new CannotDeleteOpenFileException();
            Stream stream = new FileStream(this, FileAccess.Write);
            stream.Position = FLength;
            return stream;
        }

        /// <summary>
        /// Opens the file in binary mode.
        /// </summary>
        /// <param name="access">A <see cref="System.IO.FileAccess"/> value that specifies how the file can be accessed.</param>
        /// <returns>A <see cref="System.IO.Stream"/> that can be used to access the file.</returns>
        /// <exception cref="InvalidOperationException">The file is deleted.</exception>
        [DebuggerStepThrough]
        Stream IFile.Open(FileAccess access)
        {
            CheckAlive();
            if (FIsDeleted)
                throw new CannotOpenDeletedFileException();
            return new FileStream(this, access);
        }

        /// <overloads>Opens the file in text mode for reading.</overloads>
        /// <summary>
        /// Opens the file in text mode for reading using the default encoding.
        /// </summary>
        /// <returns>A <see cref="System.IO.TextReader"/> that can be used to read from the file.</returns>
        /// <exception cref="InvalidOperationException">The file is deleted.</exception>
        [DebuggerStepThrough]
        TextReader IFile.OpenText()
        {
            CheckAlive();
            return new StreamReader(new FileStream(this, FileAccess.Read));
        }

        /// <summary>
        /// Opens the file in text mode for reading using the specified encoding.
        /// </summary>
        /// <returns>A <see cref="System.IO.TextReader"/> that can be used to read from the file.</returns>
        /// <exception cref="InvalidOperationException">The file is deleted.</exception>
        [DebuggerStepThrough]
        TextReader IFile.OpenText(System.Text.Encoding encoding)
        {
            CheckAlive();
            return new StreamReader(new FileStream(this, FileAccess.Read), encoding);
        }

        #endregion
    }
}
